﻿using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using WeavingCommon;

namespace NotifyPropertyWeaverMsBuildTask
{
    public class PropertyWeaver
    {
        MsCoreReferenceFinder msCoreReferenceFinder;
        Logger logger;
        PropertyData propertyData;
        TypeNode typeNode;
        TypeEqualityFinder typeEqualityFinder;
        MethodBody setMethodBody;
        Collection<Instruction> instructions;

        public PropertyWeaver(MsCoreReferenceFinder msCoreReferenceFinder, Logger logger, PropertyData propertyData, TypeNode typeNode, TypeEqualityFinder typeEqualityFinder)
        {
            this.msCoreReferenceFinder = msCoreReferenceFinder;
            this.logger = logger;
            this.propertyData = propertyData;
            this.typeNode = typeNode;
            this.typeEqualityFinder = typeEqualityFinder;
        }

        public void Execute()
        {
            logger.LogMessage("\t\t" + propertyData.PropertyDefinition.Name);
            var property = propertyData.PropertyDefinition;
            setMethodBody = property.SetMethod.Body;
            instructions = property.SetMethod.Body.Instructions;

            logger.LogMessage("\t\t\tCheckForEquality=" + propertyData.CheckForEquality);

            if (propertyData.BackingFieldReference == null)
            {
                foreach (var instruction in FindReturnInstructions().ToList())
                {
                    var index = instructions.IndexOf(instruction);

                    InjectAtIndex(index);
                }
            }
            else
            {
                foreach (var instruction in FindSetFieldInstructions().ToList())
                {
                    var index = instructions.IndexOf(instruction) + 1;
                    InjectAtIndex(index);
                }
            }
            if (propertyData.CheckForEquality)
            {
                AddEqualityCheck();
            }

            setMethodBody.InitLocals = true;
        }

        void InjectAtIndex(int index)
        {
            index = AddIsDirtySetterCall(index);
            var propertyDefinitions = propertyData.AlsoNotifyFor.Distinct();

            foreach (var propertyDefinition in propertyDefinitions)
            {
                index = AddEventInvokeCall(index, propertyDefinition);
            }
            AddEventInvokeCall(index, propertyData.PropertyDefinition);
        }

        IEnumerable<Instruction> FindSetFieldInstructions()
        {
            foreach (var instruction in instructions)
            {
                if (instruction.OpCode != OpCodes.Stfld)
                {
                    continue;
                }
                var fieldReference = instruction.Operand as FieldReference;
                if (fieldReference == null)
                {
                    continue;
                }

                if (fieldReference.Name == propertyData.BackingFieldReference.Name)
                {
                    yield return instruction;
                }
            }
        }
        IEnumerable<Instruction> FindReturnInstructions()
        {
            foreach (var instruction in instructions)
            {
                if (instruction.OpCode == OpCodes.Ret)
                {
                    yield return instruction;
                }
            }
        }

        int AddIsDirtySetterCall(int index)
        {
            if (typeNode.IsChangedInvoker != null &&
                propertyData.PropertyDefinition.Name != "IsChanged" &&
                propertyData.SetIsChanged)
            {
                logger.LogMessage("\t\t\tSet IsDirty");
              return  instructions.Insert(index,
                    Instruction.Create(OpCodes.Ldarg_0),
                    Instruction.Create(OpCodes.Ldc_I4, 1),
                    CreateIsChangedInvoker());
            }
            return index;
        }

        void AddEqualityCheck()
        {
            if (propertyData.BackingFieldReference == null)
            {
                CheckAgainstProperty();
            }
            else
            {
                CheckAgainstField();
            }
        }

        void CheckAgainstField()
        {
            var fieldReference = propertyData.BackingFieldReference.Resolve().GetGeneric();
            InjectEqualityCheck(Instruction.Create(OpCodes.Ldfld, fieldReference), fieldReference.FieldType);
        }


        void CheckAgainstProperty()
        {
            var propertyReference = propertyData.PropertyDefinition;
            var methodDefinition = propertyData.PropertyDefinition.GetMethod.GetGeneric();
            InjectEqualityCheck(Instruction.Create(OpCodes.Call, methodDefinition), propertyReference.PropertyType);
        }

        void InjectEqualityCheck(Instruction targetInstruction, TypeReference targetType)
        {
            var typeEqualityMethod = typeEqualityFinder.Find(targetType);

            var nopInstruction = instructions.First();
            if (nopInstruction.OpCode != OpCodes.Nop)
            {
                nopInstruction = Instruction.Create(OpCodes.Nop);
                instructions.Insert(0, nopInstruction);
            }

            if (typeEqualityMethod == null)
            {
                instructions.Prepend(
                    Instruction.Create(OpCodes.Box, targetType),
                    Instruction.Create(OpCodes.Ldarg_1),
                    Instruction.Create(OpCodes.Box, targetType),
                    Instruction.Create(OpCodes.Call, msCoreReferenceFinder.EqualsMethodReference),
                    Instruction.Create(OpCodes.Brfalse_S, nopInstruction),
                    Instruction.Create(OpCodes.Ret));
            }
            else
            {
                instructions.Prepend(
                    Instruction.Create(OpCodes.Ldarg_1),
                    Instruction.Create(OpCodes.Call, typeEqualityMethod),
                    Instruction.Create(OpCodes.Brfalse_S, nopInstruction),
                    Instruction.Create(OpCodes.Ret));
            }
            instructions.Prepend(
                Instruction.Create(OpCodes.Ldarg_0),
                targetInstruction);
        }

        int AddEventInvokeCall(int index, PropertyDefinition property)
        {
            logger.LogMessage(string.Format("\t\t\t{0}", property.Name));
            index = AddOnChangedMethodCall(index, property);
            if (typeNode.EventInvoker.IsBeforeAfter)
            {
               return AddBeforeAfrerInvokerCall(index, property);
            }
            return AddSimpleInvokerCall(index, property);
        }

        int AddOnChangedMethodCall(int index, PropertyDefinition property)
        {
            var onChangedMethod = typeNode.OnChangedMethods.FirstOrDefault(x => x.Name == string.Format("On{0}Changed", property.Name));
            if (onChangedMethod != null)
            {
               return instructions.Insert(index,
                                    Instruction.Create(OpCodes.Ldarg_0),
                                    CreateCall(onChangedMethod, onChangedMethod.IsVirtual)
                    );
            }
            return index;
        }

        int AddSimpleInvokerCall(int index, PropertyDefinition property)
        {
           return instructions.Insert(index,
                Instruction.Create(OpCodes.Ldarg_0),
                Instruction.Create(OpCodes.Ldstr, property.Name),
                CallEventInvoker());
        }

        int AddBeforeAfrerInvokerCall(int index, PropertyDefinition property)
        {
            var beforeVariable = new VariableDefinition(msCoreReferenceFinder.ObjectTypeReference);
            setMethodBody.Variables.Add(beforeVariable);
            var afterVariable = new VariableDefinition(msCoreReferenceFinder.ObjectTypeReference);
            setMethodBody.Variables.Add(afterVariable);
            var isVirtual = property.GetMethod.IsVirtual;
            var getMethod = property.GetMethod.GetGeneric();

           index = instructions.Insert(index,
                Instruction.Create(OpCodes.Ldarg_0),
                CreateCall(getMethod, isVirtual),
                //TODO: look into why this box is required
                Instruction.Create(OpCodes.Box, property.GetMethod.ReturnType),
                Instruction.Create(OpCodes.Stloc, afterVariable),
                Instruction.Create(OpCodes.Ldarg_0),
                Instruction.Create(OpCodes.Ldstr, property.Name),
                Instruction.Create(OpCodes.Ldloc, beforeVariable),
                Instruction.Create(OpCodes.Ldloc, afterVariable),
                CallEventInvoker()
                );

            instructions.Prepend(
                Instruction.Create(OpCodes.Ldarg_0),
                CreateCall(getMethod, isVirtual),
                //TODO: look into why this box is required
                Instruction.Create(OpCodes.Box, property.GetMethod.ReturnType),
                Instruction.Create(OpCodes.Stloc, beforeVariable));
            index += 4;
            return index;
        }

        public Instruction CallEventInvoker()
        {
            if (typeNode.EventInvoker.IsVirtual)
            {
                return Instruction.Create(OpCodes.Callvirt, typeNode.EventInvoker.MethodReference);
            }
            return Instruction.Create(OpCodes.Call, typeNode.EventInvoker.MethodReference);
        }

        public Instruction CreateIsChangedInvoker()
        {
            if (typeNode.IsChangedInvoker.IsVirtual)
            {
                return Instruction.Create(OpCodes.Callvirt, typeNode.IsChangedInvoker.MethodReference);
            }
            return Instruction.Create(OpCodes.Call, typeNode.IsChangedInvoker.MethodReference);
        }

        public Instruction CreateCall(MethodReference methodReference, bool isVirtual)
        {
            if (isVirtual)
            {
                return Instruction.Create(OpCodes.Callvirt, methodReference);
            }
            return Instruction.Create(OpCodes.Call, methodReference);
        }

    }
}